home *** CD-ROM | disk | FTP | other *** search
- #!/usr/bin/env python
- # -*- coding: utf-8 -*-
- """Progress handlers for APT operations"""
- # Copyright (C) 2008-2009 Sebastian Heinlein <glatzor@ubuntu.com>
- #
- # Licensed under the GNU General Public License Version 2
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # (at your option) any later version.
-
- __author__ = "Sebastian Heinlein <devel@glatzor.de>"
-
- from gettext import gettext as _
- import locale
- import logging
- import os
- import re
- import subprocess
- import sys
- import termios
- import time
- import threading
- import tty
- import warnings
-
- import apt_pkg
- import apt.progress
- import apt.debfile
- import gobject
-
- import enums
- from loop import mainloop
-
- log = logging.getLogger("AptDaemon.Worker")
- log_terminal = logging.getLogger("AptDaemon.Worker.Terminal")
-
- INSTALL_TIMEOUT = 10 * 60
-
- MAP_STAGE = {"install":_("Installing %s"),
- "configure":_("Configuring %s"),
- "remove":_("Removing %s"),
- "trigproc":_("Running post-installation trigger %s"),
- "purge":_("Purging %s"),
- "upgrade":_("Upgrading %s")}
-
- REGEX_ANSI_ESCAPE_CODE = chr(27) + "\[[;?0-9]*[A-Za-z]"
-
- class DaemonOpenProgress(apt.progress.OpProgress):
-
- """Handles the progress of the cache opening."""
-
- def __init__(self, transaction, begin=0, end=100, quiet=False):
- """Initialize a new DaemonOpenProgress instance.
-
- Keyword arguments:
- transaction -- corresponding transaction D-Bus object
- begin -- begin of the progress range (defaults to 0)
- end -- end of the progress range (defaults to 100)
- quiet -- do not emit any progress information for the transaction
- """
- apt.progress.OpProgress.__init__(self)
- self._transaction = transaction
- self.steps = [begin + (end - begin) * modifier
- for modifier in [0.12, 0.25, 0.50, 0.75, 1.00]]
- self.progress_begin = float(begin)
- self.progress_end = self.steps.pop(0)
- self.progress = 0
- self.quiet = quiet
-
- def update(self, percent):
- """Callback for progress updates.
-
- Keyword argument:
- percent - current progress in percent
- """
- progress = int(self.progress_begin + percent / 100 * \
- (self.progress_end - self.progress_begin))
- if self.progress < progress:
- if not self.quiet:
- self._transaction.progress = progress
- self.progress = progress
- while gobject.main_context_default().pending():
- gobject.main_context_default().iteration()
-
- def done(self):
- """Callback after completing a step.
-
- Sets the progress range to the next interval."""
- self.progress_begin = self.progress_end
- try:
- self.progress_end = self.steps.pop(0)
- except:
- log.warning("An additional step to open the cache is required")
-
-
- class DaemonFetchProgress(apt.progress.FetchProgress):
- '''
- Handle the package download process
- '''
- def __init__(self, transaction, begin=0, end=100):
- apt.progress.FetchProgress.__init__(self)
- self.transaction = transaction
- self.progress_end = end
- self.progress_begin = begin
- self.progress = 0
- self.items = {}
- #FIXME: This should already be part of python-apt
- self.currentItems = 0
- self.totalItems = 0
- self.currentBytes = 0
- self.totalBytes = 0
- self.currentCPS = 0
-
- def pulse(self):
- """Callback to update progress information"""
- apt.progress.FetchProgress.pulse(self)
- if self.transaction.cancelled:
- return False
- self.transaction.progress_details = (self.currentItems, self.totalItems,
- self.currentBytes, self.totalBytes,
- self.currentCPS, self.eta)
- progress = int(self.progress_begin + self.percent/100 * \
- (self.progress_end - self.progress_begin))
- # If the progress runs backwards emit an illegal progress value
- # e.g. during cache updates.
- if self.progress > progress:
- self.transaction.progress = 101
- else:
- self.transaction.progress = progress
- self.progress = progress
- while gobject.main_context_default().pending():
- gobject.main_context_default().iteration()
- return True
-
- def updateStatus(self, uri, descr, shortDescr, status):
- """Callback to update the status information"""
- if status != self.dlQueued:
- log.debug("%s %s" % (self.dlStatusStr[status], uri))
- if status == self.dlQueued:
- self.transaction.status_details = _("Downloading %s") % shortDescr
- self.items[uri] = status
-
- def start(self):
- """Callback at the beginning of the operation"""
- self.transaction.status = enums.STATUS_DOWNLOADING
- self.transaction.allow_cancel = True
-
- def stop(self):
- """Callback at the end of the operation"""
- self.transaction.progress_details = (0, 0, 0, 0, 0, 0)
- self.transaction.progress = self.progress_end
- self.transaction.allow_cancel = False
-
- def mediaChange(self, medium, drive):
- """Callback for media changes"""
- #FIXME: make use of DeviceKit/hal
- self.transaction.required_medium = medium, drive
- self.transaction.paused = True
- self.transaction.status = enums.STATUS_WAITING_MEDIUM
- while self.transaction.paused:
- gobject.main_context_default().iteration()
- self.transaction.status = enums.STATUS_DOWNLOADING
- if self.transaction.cancelled:
- return False
- return True
-
-
- class DaemonInstallProgress(object):
-
- def __init__(self, transaction, begin=50, end=100):
- self.transaction = transaction
- self.status = ""
- self.progress = 0
- self.progress_begin = begin
- self.progress_end = end
- self._child_exit = -1
- self.last_activity = 0
- self.child_pid = 0
- self.status_parent_fd, self.status_child_fd = os.pipe()
- self.output = ""
- self._line_buffer = ""
-
- def startUpdate(self):
- log.debug("Start update")
- try:
- apt_pkg.PkgSystemUnLock()
- except SystemError:
- pass
- self.transaction.status = enums.STATUS_COMMITTING
- self.transaction.allow_terminal = True
- self.last_activity = time.time()
- self.start_time = time.time()
-
- def finishUpdate(self):
- """Callback at the end of the operation"""
- #if self.conffile_prompts:
- # self._transaction.Message(MESSAGE_CONFIG_FILES_CHANGED,
- # "The following conffile prompts were found "
- # "and need investiagtion: %s" % \
- # "\n".join(self.conffile_prompts))
- # Check for required restarts
- #if os.path.exists("/var/run/reboot-required") and \
- # os.path.getmtime("/var/run/reboot-required") > self.start_time:
- #self._transaction.RequireRestart(RESTART_SYSTEM, "")
- self.transaction.allow_terminal = False
-
- def _child(self, pm):
- try:
- res = pm.DoInstall(self.status_child_fd)
- except:
- os._exit(pm.ResultFailed)
- else:
- os._exit(res)
-
- def run(self, *args, **kwargs):
- log.debug("Run")
- pid = self._fork()
- if pid == 0:
- os.close(self.status_parent_fd)
- self._child(*args, **kwargs)
- else:
- self.child_pid = pid
- os.close(self.status_child_fd)
- log.debug("Child pid: %s", pid)
- watchers = []
- flags = gobject.IO_IN | gobject.IO_ERR | gobject.IO_HUP
- if self.transaction.terminal:
- # Save the settings of the transaction terminal and set to raw mode
- terminal_fd = os.open(self.transaction.terminal,
- os.O_RDWR|os.O_NOCTTY|os.O_NONBLOCK)
- terminal_attr = termios.tcgetattr(terminal_fd)
- tty.setraw(terminal_fd, termios.TCSANOW)
- # Setup copying of i/o between the controlling terminals
- watchers.append(gobject.io_add_watch(terminal_fd, flags,
- self._copy_io))
- else:
- terminal_fd = None
- watchers.append(gobject.io_add_watch(self.master_fd, flags,
- self._copy_io_master, terminal_fd))
- # Monitor the child process
- watchers.append(gobject.child_watch_add(pid, self._on_child_exit))
- # Watch for status updates
- watchers.append(gobject.io_add_watch(self.status_parent_fd,
- gobject.IO_IN,
- self._on_status_update))
- while self._child_exit == -1:
- gobject.main_context_default().iteration()
- for id in watchers:
- gobject.source_remove(id)
- # Restore the settings of the transaction terminal
- try:
- termios.tcsettattr(terminal_fd, termios.TCSADRAIN, terminal_attr)
- except:
- pass
- # Make sure all file descriptors are closed
- for fd in [self.master_fd, self.status_parent_fd, terminal_fd]:
- try:
- os.close(fd)
- except:
- pass
- return os.WEXITSTATUS(self._child_exit)
-
- def _on_child_exit(self, pid, condition):
- log.debug("Child exited: %s", condition)
- self._child_exit = condition
- return False
-
- def _on_status_update(self, source, condition):
- log.debug("UpdateInterface")
- status_msg = ""
- try:
- while not status_msg.endswith("\n"):
- self.last_activity = time.time()
- status_msg += os.read(source, 1)
- except:
- return False
- try:
- (status, pkg, percent, message_raw) = status_msg.split(":", 3)
- except ValueError:
- # silently ignore lines that can't be parsed
- return True
- message = message_raw.strip()
- #print "percent: %s %s" % (pkg, float(percent)/100.0)
- if status == "pmerror":
- self._error(pkg, message)
- elif status == "pmconffile":
- # we get a string like this:
- # 'current-conffile' 'new-conffile' useredited distedited
- match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", message_raw)
- if match:
- new, old = match.group(1), match.group(2)
- self._conffile(new, old)
- elif status == "pmstatus":
- self._status_changed(pkg, float(percent), message)
- # catch a time out by sending crtl+c
- if self.last_activity + INSTALL_TIMEOUT < time.time() and \
- self.child_pid:
- log.critical("Killing child since timeout of %s s", INSTALL_TIMEOUT)
- os.kill(self.child_pid, 15)
- return True
-
- def _fork(self):
- """Fork and create a master/slave pty pair by which the forked process
- can be controlled.
- """
- # process all pending events in the main loop, since we will quit
- # the loop in the child process
- context = gobject.main_context_default()
- while context.pending():
- context.iteration()
- pid, self.master_fd = os.forkpty()
- if pid == 0:
- mainloop.quit()
- # Switch to the language of the user
- if self.transaction.locale:
- os.putenv("LANG", self.transaction.locale)
- # Either connect to the controllong terminal or switch to
- # non-interactive mode
- if not self.transaction.terminal:
- # FIXME: we should check for "mail" or "gnome" here
- # and not unset in this case
- os.putenv("APT_LISTCHANGES_FRONTEND", "none")
- else:
- #FIXME: Should this be a setting?
- os.putenv("TERM", "linux")
- # Run debconf through a proxy if available
- if self.transaction.debconf:
- os.putenv("DEBCONF_PIPE", self.transaction.debconf)
- os.putenv("DEBIAN_FRONTEND", "passthrough")
- if log.level == logging.DEBUG:
- os.putenv("DEBCONF_DEBUG",".")
- elif not self.transaction.terminal:
- os.putenv("DEBIAN_FRONTEND", "noninteractive")
- return pid
-
- def _copy_io_master(self, source, condition, target):
- if condition == gobject.IO_IN:
- self.last_activity = time.time()
- char = os.read(source, 1)
- # Write all the output from dpkg to a log
- if char == "\n":
- # Skip ANSI characters from the console output
- line = re.sub(REGEX_ANSI_ESCAPE_CODE, "", self._line_buffer)
- if line:
- log_terminal.debug(line)
- self.output += line + "\n"
- self._line_buffer = ""
- else:
- self._line_buffer += char
- if target:
- try:
- os.write(target, char)
- except:
- pass
- return True
- os.close(source)
- return False
-
- def _copy_io(self, source, condition):
- if condition == gobject.IO_IN:
- char = os.read(source, 1)
- # Detect config file prompt answers on the console
- # FIXME: Perhaps should only set the
- # self.transaction.config_file_prompt_answer and not write
- if self.transaction.paused and \
- self.transaction.config_file_prompt:
- self.transaction.config_file_prompt_answer = None
- self.transaction.paused = False
- try:
- os.write(self.master_fd, char)
- except:
- pass
- else:
- return True
- os.close(source)
- return False
-
- def _status_changed(self, pkg, percent, status):
- """Callback to update status information"""
- log.debug("APT status: %s" % status)
- progress = self.progress_begin + percent / 100 * \
- (self.progress_end - self.progress_begin)
- if self.progress < progress:
- self.transaction.progress = int(progress)
- self.progress = progress
- self.transaction.status_details = status
-
- def _conffile(self, current, new):
- """Callback for a config file conflict"""
- log.warning("Config file prompt: '%s' (%s)" % (current, new))
- self.transaction.config_file_prompt = (current, new)
- self.transaction.paused = True
- self.transaction.status = enums.STATUS_WAITING_CONFIG_FILE_PROMPT
- while self.transaction.paused:
- gobject.main_context_default().iteration()
- log.info("Sending config file answer: %s",
- self.transaction.config_file_prompt_answer)
- if self.transaction.config_file_prompt_answer == "replace":
- os.write(self.master_fd, "y\n")
- elif self.transaction.config_file_prompt_answer == "keep":
- os.write(self.master_fd, "n\n")
- self.transaction.config_file_prompt_answer = None
- self.transaction.config_file_prompt = None
- self.transaction.status = enums.STATUS_COMMITTING
- return True
-
- def _error(self, pkg, msg):
- """Callback for an error"""
- log.critical("%s: %s" % (pkg, msg))
-
-
- class DaemonDpkgInstallProgress(DaemonInstallProgress):
-
- """Progress handler for a local Debian package installation."""
-
- def _child(self, debfile):
- args = ["/usr/bin/dpkg", "--status-fd", str(self.status_child_fd)]
- if not self.transaction.terminal:
- args.extend(["--force-confdef", "--force-confold"])
- args.extend(["-i", debfile])
- os.execlp("/usr/bin/dpkg", *args)
- # We should never go here
- os._exit()
-
- def _on_status_update(self, source, condition):
- log.debug("UpdateInterface")
- status_raw = ""
- try:
- while not status_raw.endswith("\n"):
- status_raw += os.read(source, 1)
- except:
- return False
- try:
- status = [s.strip() for s in status_raw.split(":", 3)]
- except ValueError:
- # silently ignore lines that can't be parsed
- return True
- # Parse the status message. It can be of the following types:
- # - "status: PACKAGE: STATUS"
- # - "status: PACKAGE: error: MESSAGE"
- # - "status: FILE: conffile: 'OLD' 'NEW' useredited distedited"
- # - "processing: STAGE: PACKAGE" with STAGE is one of upgrade,
- # install, configure, trigproc, remove, purge
- if status[0] == "status":
- if status[2] == "error":
- self._error(status[1], status[3])
- elif status[2] == "conffile":
- match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", status[3])
- if match:
- new, old = match.group(1), match.group(2)
- self._conffile(new, old)
- elif status == "status":
-
- self._status_changed(pkg=status[1], percent=0,
- status=status[2])
- elif status[0] == "processing":
- try:
- msg = MAP_STAGE[status[1]] % status[2]
- except ValueError, IndexError:
- msg = status[1]
- self._status_changed(pkg=status[2], percent=0, status=msg)
-
-
- class DaemonDpkgRecoverProgress(DaemonDpkgInstallProgress):
-
- """Progress handler for dpkg --confiure -a call."""
-
- def _child(self):
- args = ["/usr/bin/dpkg", "--status-fd", str(self.status_child_fd),
- "--configure", "-a"]
- if not self.transaction.terminal:
- args.extend(["--force-confdef", "--force-confold"])
- os.execlp("/usr/bin/dpkg", *args)
- # We should never go here
- os._exit()
-
-
- # vim:ts=4:sw=4:et
-